// Copyright 2021 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'dart:async';

import 'package:devtools_app/devtools_app.dart';
import 'package:devtools_app/src/shared/console/eval/auto_complete.dart';
import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_test/helpers.dart';
import 'package:flutter_test/flutter_test.dart';

import '../../test_infra/flutter_test_driver.dart';
import '../../test_infra/flutter_test_environment.dart';
import '../../test_infra/flutter_test_storage.dart';

void main() {
  setGlobal(Storage, FlutterTestStorage());

  final env = FlutterTestEnvironment(
    const FlutterRunConfiguration(withDebugger: true),
  );

  late Disposable isAlive;
  late DebuggerController debuggerController;
  late EvalOnDartLibrary eval;

  setUp(() async {
    setGlobal(
      DevToolsEnvironmentParameters,
      ExternalDevToolsEnvironmentParameters(),
    );
    setGlobal(BreakpointManager, BreakpointManager());
    setGlobal(EvalService, EvalService());
    setGlobal(NotificationService, NotificationService());
    isAlive = Disposable();
    await env.setupEnvironment();
    debuggerController = DebuggerController();

    eval = EvalOnDartLibrary(
      'package:flutter_app/src/autocomplete.dart',
      serviceConnection.serviceManager.service!,
      serviceManager: serviceConnection.serviceManager,
      disableBreakpoints: false,
    );
  });

  tearDown(() async {
    await debuggerController.resume();
    isAlive.dispose();
    debuggerController.dispose();
    await env.tearDownEnvironment();
  });

  tearDownAll(() async {
    await env.tearDownEnvironment(force: true);
  });

  Future<void> runMethodAndWaitForPause(String method) async {
    unawaited(eval.eval(method, isAlive: isAlive));

    await whenMatches(debuggerController.selectedStackFrame, (f) => f != null);
  }

  group('EvalOnDartLibrary', () {
    test(
      'returns scoped variables when EditingParts is not a field',
      () async {
        await runMethodAndWaitForPause(
          'AnotherClass().pauseWithScopedVariablesMethod()',
        );
        expect(
          await autoCompleteResultsFor(
            EditingParts(activeWord: 'foo', leftSide: '', rightSide: ''),
            evalService,
          ),
          equals(['foo', 'foobar']),
        );
        expect(
          await autoCompleteResultsFor(
            EditingParts(activeWord: 'b', leftSide: '', rightSide: ''),
            evalService,
          ),
          equals(['bar', 'baz']),
        );
      },
      timeout: const Timeout.factor(8),
      tags: includeForCustomerTestsTag,
    );

    test(
      'returns filtered members when EditingParts is a field ',
      () async {
        await runMethodAndWaitForPause(
          'AnotherClass().pauseWithScopedVariablesMethod()',
        );
        expect(
          await autoCompleteResultsFor(
            EditingParts(activeWord: 'f', leftSide: 'foo.', rightSide: ''),
            evalService,
          ),
          equals(['field1', 'field2', 'func1', 'func2']),
        );
        expect(
          await autoCompleteResultsFor(
            EditingParts(activeWord: 'fu', leftSide: 'foo.', rightSide: ''),
            evalService,
          ),
          equals(['func1', 'func2']),
        );
      },
      timeout: const Timeout.factor(8),
      tags: includeForCustomerTestsTag,
    );

    test(
      'returns filtered members when EditingParts is a class name ',
      () async {
        await runMethodAndWaitForPause(
          'AnotherClass().pauseWithScopedVariablesMethod()',
        );
        expect(
          await autoCompleteResultsFor(
            EditingParts(leftSide: 'FooClass.', activeWord: '', rightSide: ''),
            evalService,
          ),
          equals([
            'staticField1',
            'staticField2',
            'namedConstructor',
            'factory1',
            'staticMethod',
          ]),
        );
        expect(
          await autoCompleteResultsFor(
            EditingParts(
              activeWord: 'fa',
              leftSide: 'FooClass.',
              rightSide: '',
            ),
            evalService,
          ),
          equals(['factory1']),
        );
      },
      timeout: const Timeout.factor(8),
      tags: includeForCustomerTestsTag,
    );
    test(
      'returns privates only from library',
      // TODO(https://github.com/flutter/devtools/issues/7099): unskip once
      // this test flake is fixed.
      skip: true,
      () async {
        await runMethodAndWaitForPause(
          'AnotherClass().pauseWithScopedVariablesMethod()',
        );
        await expectLater(
          autoCompleteResultsFor(
            EditingParts(activeWord: '_', leftSide: '', rightSide: ''),
            evalService,
          ),
          completion(
            unorderedEquals([
              '_privateField2',
              '_privateField1',
              '_PrivateClass',
            ]),
          ),
        );
      },
      timeout: const Timeout.factor(8),
      tags: includeForCustomerTestsTag,
    );
    test(
      'returns exported members from import',
      () async {
        await runMethodAndWaitForPause(
          'AnotherClass().pauseWithScopedVariablesMethod()',
        );
        expect(
          await autoCompleteResultsFor(
            EditingParts(
              activeWord: 'exportedField',
              leftSide: '',
              rightSide: '',
            ),
            evalService,
          ),
          equals(['exportedField']),
        );

        expect(
          await autoCompleteResultsFor(
            EditingParts(
              activeWord: 'ExportedClass',
              leftSide: '',
              rightSide: '',
            ),
            evalService,
          ),
          equals(['ExportedClass']),
        );

        // Privates are not exported
        expect(
          await autoCompleteResultsFor(
            EditingParts(
              activeWord: '_privateExportedField',
              leftSide: '',
              rightSide: '',
            ),
            evalService,
          ),
          equals([]),
        );

        expect(
          await autoCompleteResultsFor(
            EditingParts(
              activeWord: '_PrivateExportedClass',
              leftSide: '',
              rightSide: '',
            ),
            evalService,
          ),
          equals([]),
        );
      },
      timeout: const Timeout.factor(8),
      tags: includeForCustomerTestsTag,
    );

    test(
      'returns prefixes of libraries imported',
      () async {
        await runMethodAndWaitForPause(
          'AnotherClass().pauseWithScopedVariablesMethod()',
        );
        expect(
          await autoCompleteResultsFor(
            EditingParts(activeWord: 'developer', leftSide: '', rightSide: ''),
            evalService,
          ),
          equals(['developer']),
        );

        expect(
          await autoCompleteResultsFor(
            EditingParts(activeWord: 'math', leftSide: '', rightSide: ''),
            evalService,
          ),
          equals(['math']),
        );
      },
      timeout: const Timeout.factor(8),
      tags: includeForCustomerTestsTag,
    );

    test(
      'returns no operators for int',
      () async {
        await runMethodAndWaitForPause(
          'AnotherClass().pauseWithScopedVariablesMethod()',
        );
        expect(
          await autoCompleteResultsFor(
            EditingParts(leftSide: '7.', activeWord: '', rightSide: ''),
            evalService,
          ),
          equals([
            'hashCode',
            'bitLength',
            'toString',
            'remainder',
            'abs',
            'sign',
            'isEven',
            'isOdd',
            'isNaN',
            'isNegative',
            'isInfinite',
            'isFinite',
            'toUnsigned',
            'toSigned',
            'compareTo',
            'round',
            'floor',
            'ceil',
            'truncate',
            'roundToDouble',
            'floorToDouble',
            'ceilToDouble',
            'truncateToDouble',
            'clamp',
            'toInt',
            'toDouble',
            'toStringAsFixed',
            'toStringAsExponential',
            'toStringAsPrecision',
            'toRadixString',
            'modPow',
            'modInverse',
            'gcd',
            'noSuchMethod',
            'runtimeType',
          ]),
        );
      },
      timeout: const Timeout.factor(8),
      tags: includeForCustomerTestsTag,
    );
  });
}
